[None][feat] Add DeepSeek DSpark speculative decoding#15808
Conversation
533cd80 to
c776eb5
Compare
📝 WalkthroughWalkthroughThis PR adds a new DSpark speculative decoding mode across the TensorRT-LLM ChangesDSpark Speculative Decoding
Sequence Diagram(s)sequenceDiagram
participant TargetModel as DeepseekV4DecoderLayer
participant SpecMetadata as DSparkSpecMetadata
participant DSparkWorker
participant DraftModel as DSparkDraftModel
TargetModel->>SpecMetadata: maybe_capture_hidden_states(resolved_residual)
DSparkWorker->>SpecMetadata: prepare(request_ids)
DSparkWorker->>SpecMetadata: get_hidden_states()
DSparkWorker->>DSparkWorker: write_context_windows / back-fill accepted tokens
DSparkWorker->>DraftModel: forward_batched(main_hidden, bonus_token_ids, kv_windows, slots)
DraftModel->>DraftModel: _forward_stage (attention + MoE) per stage
DraftModel->>DraftModel: forward_head (dspark_propose)
DraftModel-->>DSparkWorker: draft tokens, num_proposed
DSparkWorker-->>TargetModel: next_draft_tokens
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py (1)
151-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCoverage is insufficient for confidence-based truncation in the worker.
These tests only cover slot/window bookkeeping. They never stub
draft_model.forward()/forward_batched()to returnnum_proposed < block_size, so the worker contract that should honor DSpark truncation is still untested. Please add that regression intests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py(and, if you want end-to-end coverage, follow up intests/unittest/_torch/speculative/hw_agnostic/test_dspark.py).As per path instructions, coverage here is insufficient and the feedback should name the target test files explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py` around lines 151 - 210, The current tests only verify slot/window bookkeeping and do not cover the confidence-based truncation path in the worker. Add a regression test in test_dspark_worker that stubs draft_model.forward() or forward_batched() to return num_proposed less than block_size, then assert the worker honors DSpark truncation behavior; use _lazy_init, prepare, and the draft model call path to locate the right spot. If you want broader coverage, add a matching end-to-end test in test_dspark as well.Source: Path instructions
tests/unittest/_torch/test_model_config.py (1)
173-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDSpark config coverage is still missing.
This regression closes the
compress_ratios=Nonegap, but none of the new DSpark validation intensorrt_llm/llmapi/llm_args.pyis covered here. Please add focused cases intests/unittest/_torch/test_model_config.pyor a newtests/unittest/llmapi/test_llm_args.pyfor same-checkpointspeculative_modelhandling and rejectingblock_size != max_draft_len, so the first signal is not the 8-GPU e2e.As per path instructions, "
tests/**: Act as a QA engineer reviewing test changes and coverage for TensorRT-LLM. Keep feedback actionable: suggest concrete list file names and whether coverage is sufficient, insufficient, or needs follow-up outside the PR."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unittest/_torch/test_model_config.py` around lines 173 - 198, Coverage is insufficient for the new DSpark validation in llm_args.py. Add focused tests in tests/unittest/_torch/test_model_config.py or a new tests/unittest/llmapi/test_llm_args.py that exercise the speculative_model same-checkpoint path and verify block_size is rejected when it differs from max_draft_len. Use the relevant entry points ModelConfig.from_pretrained and the llm_args validation logic so the failure is caught in unit tests instead of only in e2e.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tensorrt_llm/_torch/models/modeling_dspark.py`:
- Around line 367-377: The DSpark confidence path is always enabling
`with_markov` even when `build_markov_head()` returns `None`, which later makes
`DSparkConfidenceHead.forward()` assert on missing `prev_embeddings`. Update the
initialization in `DSparkModel` so `with_markov` is derived from whether
`self.markov_head` actually exists (or from `self.markov_rank > 0`), and pass
that value into `DSparkConfidenceHead` instead of hardcoding `True`. This should
keep `dspark_propose()` and `DSparkConfidenceHead` aligned when
`dspark_markov_rank=0`.
In `@tensorrt_llm/_torch/speculative/dspark.py`:
- Around line 339-347: `num_proposed` is being computed by
`draft_model.forward*()` but then discarded, so speculative scheduling still
always queues a full `block_size` block. Update the DSpark flow in `dspark.py`
to capture the second return value from both draft paths and thread it through
`next_draft_tokens` / the verifier so the scheduled draft length is truncated
according to `confidence_threshold`. Use the existing `draft_model.forward*()`,
`next_draft_tokens`, and speculative verification logic to ensure the count
affects runtime behavior, then add a regression test covering truncated draft
blocks.
In `@tensorrt_llm/llmapi/llm_args.py`:
- Around line 5137-5179: Move the DSpark validation into this
`DSparkDecodingConfig` handling block in `llm_args.py` so misconfigurations fail
early: explicitly reject `speculative_model=None` when `speculative_config` is
`DSparkDecodingConfig`, and validate that the resolved `block_size` matches
`max_draft_len` after loading any draft config overrides. Keep the checks close
to the existing `spec_cfg` resolution logic and use the `DSparkDecodingConfig` /
`speculative_model` / `max_draft_len` symbols so the invariant is enforced
before downstream DSpark model construction.
- Around line 2479-2484: The confidence_threshold field in llm_args.py is
currently unconstrained, so it can accept values outside the valid probability
range and cause DSpark truncation misconfiguration. Update the
confidence_threshold definition in the relevant args/model to enforce a [0, 1]
bound using Pydantic field constraints, and keep the existing default and
description intact. Use the confidence_threshold symbol to locate the field and
ensure the validation is declarative rather than handled by a custom validator.
In `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark_cuda_graph.py`:
- Around line 214-252: The CUDA graph test only captures the non-persistent
branch, so it misses the production write-through path in
dspark_attention_forward_batched. Update
test_batched_attention_cuda_graph_capture_replay to also exercise persist=True
with a separate cache buffer, using the same CUDA graph capture/replay pattern.
Add an assertion that verifies the expected rolling KV window mutation after
replay, alongside the existing output comparison.
In `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark.py`:
- Around line 107-114: Wrap the DSpark speculative decoding section in a
try/finally so the 8-GPU LLM created in spec_llm is always shut down even if
generate() or the output extraction for
spec_out/spec_texts/spec_ids/avg_accepted fails. Keep the existing spec_config,
spec_llm.generate, and shutdown logic, but move spec_llm.shutdown() into the
finally block to prevent leaked model/process-group state.
---
Nitpick comments:
In `@tests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.py`:
- Around line 151-210: The current tests only verify slot/window bookkeeping and
do not cover the confidence-based truncation path in the worker. Add a
regression test in test_dspark_worker that stubs draft_model.forward() or
forward_batched() to return num_proposed less than block_size, then assert the
worker honors DSpark truncation behavior; use _lazy_init, prepare, and the draft
model call path to locate the right spot. If you want broader coverage, add a
matching end-to-end test in test_dspark as well.
In `@tests/unittest/_torch/test_model_config.py`:
- Around line 173-198: Coverage is insufficient for the new DSpark validation in
llm_args.py. Add focused tests in tests/unittest/_torch/test_model_config.py or
a new tests/unittest/llmapi/test_llm_args.py that exercise the speculative_model
same-checkpoint path and verify block_size is rejected when it differs from
max_draft_len. Use the relevant entry points ModelConfig.from_pretrained and the
llm_args validation logic so the failure is caught in unit tests instead of only
in e2e.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: fec2a059-f520-48da-ac10-e69c8dd54c96
📒 Files selected for processing (22)
tensorrt_llm/_torch/model_config.pytensorrt_llm/_torch/models/modeling_deepseekv4.pytensorrt_llm/_torch/models/modeling_dspark.pytensorrt_llm/_torch/models/modeling_speculative.pytensorrt_llm/_torch/modules/mhc/hyper_connection.pytensorrt_llm/_torch/speculative/dspark.pytensorrt_llm/_torch/speculative/dspark_attention.pytensorrt_llm/_torch/speculative/dspark_draft.pytensorrt_llm/_torch/speculative/dspark_heads.pytensorrt_llm/_torch/speculative/interface.pytensorrt_llm/_torch/speculative/utils.pytensorrt_llm/llmapi/__init__.pytensorrt_llm/llmapi/llm_args.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_attention.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_cuda_graph.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_draft.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_heads.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_weight_schema.pytests/unittest/_torch/speculative/hw_agnostic/test_dspark_worker.pytests/unittest/_torch/test_model_config.pytests/unittest/api_stability/references_committed/llm.yaml
5e177ce to
c8fcd3d
Compare
|
/bot run |
|
PR_Github #57370 [ run ] triggered by Bot. Commit: |
|
PR_Github #57370 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #57435 [ run ] triggered by Bot. Commit: |
|
PR_Github #57435 [ run ] completed with state
|
15169d5 to
fa6461a
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #57501 [ run ] triggered by Bot. Commit: |
|
PR_Github #57501 [ run ] completed with state |
28e16ea to
c0c2ab0
Compare
623e086 to
c0b95fe
Compare
pengbowang-nv
left a comment
There was a problem hiding this comment.
Approve for attention part
|
LGTM in models part. |
BowenFu
left a comment
There was a problem hiding this comment.
LGTM. Reviewed for regressions to existing/default (non-DSpark) behavior: all shared-path changes are either DSpark-gated (is_dspark()) or provably behavior-preserving — the modeling_deepseekv4.py deletions are a verbatim closure→helper refactor, the widened allgather payloads unpack consistently, and the mla/hyper_connection changes are no-ops for existing inputs. API additions are purely additive with the manifest updated. Non-DSpark paths unaffected.
Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
Make the checkpoint's dspark_target_layer_ids authoritative for DSpark speculative decoding. When target_layer_ids is unset it is copied from the checkpoint verbatim (order preserved, since main_proj columns are order-dependent). An explicit override that does not exactly match the checkpoint list is now rejected during config validation instead of failing later with a projection shape mismatch (different count) or silently degrading acceptance (same count, different/reordered layers). Add unit coverage for defaulting, matching override, mismatched count, same-count-different-layers, and order-mismatch cases. Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
Rebase DSpark onto the unified one-model speculative-decoding base introduced by PR NVIDIA#15775 and route production and acceptance through the shared entry points instead of hand-rolling them in DSparkWorker: - Produce draft tokens via SpecWorkerBase.sample_draft_tokens on the per-position corrected block logits [num_gens, K, vocab] (surfaced from the draft model via the existing return_logits path), letting the base do greedy/rejection sampling, TP gather, d2t, and the draft_probs scatter. - Accept via SpecWorkerBase.sample_and_accept_draft_tokens (strict or rejection), dropping the local _sample_and_accept_draft_tokens_base call. - Fill context requests draft-prob rows with a one-hot via write_context_onehot_draft_probs. - Register DSpark in the rejection gate (is_supported / is_new_rejection_method) and thread vocab_size / draft_vocab_size / use_rejection_sampling into DSparkSpecMetadata so the slot-indexed rejection buffers are allocated. Greedy behavior is preserved (the base argmaxes the same corrected logits the head produced). Adds a mixed-batch (context + gen) worker orchestration test. Validated: 60/60 hw_agnostic unit tests; 8xB300 e2e (BENCH_OK); and the full DL1-7 thinking on/off DEP8+MegaMoE-DeepGEMM AL sweep matches the previously collected baseline within N=12 nondeterministic variance. Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
The optional real-fp8-MLA draft-attention path (env TLLM_DSPARK_REAL_MLA /
DSparkDraftModel.use_real_mla) was opt-in, never used by any production or
benchmark config (all use the default bf16 captured-context path), and is
currently broken: it runs torch.einsum/baddbmm on Float8_e4m3fn tensors, which
torch does not implement ("baddbmm_cuda not implemented for Float8_e4m3fn").
It is not on the roadmap, so remove it rather than ship dead/broken code.
Removed:
- _dspark_mla_attention() and the TLLM_DSPARK_REAL_MLA env / use_real_mla attr
and all use_real_mla branches in modeling_dspark.py (attention dispatch,
weight-load setup, and the forward_batched NotImplementedError guard).
- The eager per-request DSparkWorker._draft_gen_block fallback (which existed
only for real-fp8-MLA) and the _use_batched toggle; the worker is now always
the batched, CUDA-graph-safe path.
- The obsolete legacy-real-mla worker test.
The default bf16 batched path is unchanged (byte-identical, just un-nested).
Validated: 59/59 DSpark hw_agnostic unit tests pass (worker/draft/heads/
attention/cuda_graph). Net -256 lines.
Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
… width Address PR review (r3601639329). DSparkWorker.forward passed gen_logits.shape[-1] to write_context_onehot_draft_probs, but DSpark surfaces vocab-sharded draft logits (unlike DFlash, which gathers in the worker), so under tensor parallelism that is the pre-gather shard width. sample_draft_tokens all-gathers the logits to full vocab and scatters draft_probs at that full width (publishing draft_probs_last_dim), so writing the context requests' one-hot at the shard width leaves the trailing columns stale and corrupts rejection acceptance on context-to-gen transitions. Use spec_metadata.draft_probs_last_dim (the width the gen scatter just published) for the context one-hot. Update the mixed-batch worker test to assert the one-hot uses the scatter width, not the sharded gen_logits width. Only affects the rejection-sampling path under plain TP (no-op for greedy and gated off under attention-DP); validated by the DSpark hw_agnostic unit suite (59 passed). Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
…onfig Confidence-based dynamic drafting is intentionally not enabled in this PR (the draft path always calls with confidence_threshold=0.0). Keep the confidence head as scaffolding for a follow-up, but address the related review feedback and remove the user-facing knobs that are not wired up yet: - Gate DSparkConfidenceHead(with_markov=...) on markov_rank > 0 (was hardcoded True). build_markov_head() returns None for markov_rank <= 0, and dspark_propose then passes no prev_embeddings, which would trip the prev_embeddings assert in DSparkConfidenceHead.forward (review r3502819934). - Remove the unwired DSparkDecodingConfig fields enable_confidence_head and confidence_threshold (read nowhere at runtime; review r3547358128). They will be re-added when the confidence head is actually wired into the speculative scheduler/verifier. Regenerate the LLM args golden manifest accordingly. - Document in dspark_propose that num_proposed is not yet consumed and the confidence branch is inert scaffolding (review r3502819941). The confidence head module and its internal plumbing remain in place. No runtime behavior change (the confidence path was already never exercised). Validated: DSpark hw_agnostic unit suite (24 passed) and test_llm_args DSpark tests (9 passed). Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
f4596f2 to
e676f36
Compare
|
/bot run |
|
PR_Github #59961 [ run ] triggered by Bot. Commit: |
juney-nvidia
left a comment
There was a problem hiding this comment.
Approved from API and telemetry perspectives.
|
PR_Github #59961 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #59986 [ run ] triggered by Bot. Commit: |
|
PR_Github #59986 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #60059 [ run ] triggered by Bot. Commit: |
|
PR_Github #60059 [ run ] completed with state |
Consolidate the DSpark dev-branch documentation into a single commit and update it to the post-merge reality of PR NVIDIA#15808: - DSPARK_DEV.md: note the PR is merged; describe the confidence head as loaded scaffolding (truncation not wired); drop the removed real-fp8-MLA (TLLM_DSPARK_REAL_MLA) path from the design and acceptance sections. - DSPARK_REVIEW.md: prune the items resolved in / after the merge (target-layer validation, gating corrected logits on return_logits, with_markov/markov_rank head consistency, real-fp8-MLA removal, and removal of the unwired enable_confidence_head / confidence_threshold config fields); reword the confidence-truncation and duplicate-weight items for the current code. These notes live on the development branch only and are not part of any PR. Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
Consolidate the DSpark dev-branch documentation into a single commit and update it to the post-merge reality of PR NVIDIA#15808: - DSPARK_DEV.md: note the PR is merged; describe the confidence head as loaded scaffolding (truncation not wired); drop the removed real-fp8-MLA (TLLM_DSPARK_REAL_MLA) path from the design and acceptance sections. - DSPARK_REVIEW.md: prune the items resolved in / after the merge (target-layer validation, gating corrected logits on return_logits, with_markov/markov_rank head consistency, real-fp8-MLA removal, and removal of the unwired enable_confidence_head / confidence_threshold config fields); reword the confidence-truncation and duplicate-weight items for the current code. These notes live on the development branch only and are not part of any PR. Signed-off-by: Jonas Li <6110159+longlee0622@users.noreply.github.com>
Summary
Adds DeepSeek DSpark speculative decoding (the DeepSeek-V4-Pro-DSpark draft) to the PyTorch backend.
DSpark extends the MTP one-model family with three full DeepSeek-V4 draft blocks under the mtp.* namespace. One backbone forward proposes a block of tokens, a lightweight sequential Markov head refines the proposal, and a per-position confidence head can truncate it. Acceptance still uses standard target verification, so greedy correctness is preserved independently of draft quality.
The implementation runs end-to-end on 8×B300 as a one-engine drafter, and the generation draft path can be captured into the target CUDA graph.
What is added
CUDA-graph safety
The default generation draft path is batched and avoids host synchronization and data-dependent shapes. It uses per-request start-position tensors, RoPE gathered from a fixed table, slot-indexed rolling windows, and fixed-width masked top-k. The batched path is numerically checked against the scalar reference path.
No DSpark-specific CUDA-graph flag is required.
Validated LLM API configuration
The new full-accuracy LLM API test uses the following topology and backend:
The draft inherits the target MoE backend. The validated high-acceptance path is DEP8 + MegaMoE DeepGEMM. CUTLASS remains a compatible fallback; TRTLLM-Gen blockScaleMoe cannot route this checkpoint's 384-expert / 8-group layout.
Correctness and accuracy
The 8-GPU MoE path is not bit-reproducible across independent runs because of non-associative FP atomics, so the end-to-end test does not require token-for-token equality with a separate no-spec run. The verification invariant is covered by unit tests and numerical goldens.
Acceptance length: DL1-DL7 sweep
Measured offline on 8×B300 with the same TP8 + EP8 + attention DP + MegaMoE DeepGEMM configuration as the LLM API test. The sweep used greedy decoding, 12 prompts per dataset / thinking mode / draft length, up to 512 new tokens, with chunked prefill and CUDA graph disabled.
AL is avg_decoded_tokens_per_iter: committed tokens per target step, including the one guaranteed target token. DL is max_draft_len, so AL is in [1, DL+1]. Each dataset cell below is Thinking-OFF / Thinking-ON.
Key observations:
All 42 reported AL values were validated against the raw log, all seven draft-length runs exited successfully, and the sweep completed successfully.
Notes and limitations
PR checklist